package xyz.zhno.template.common.lock.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import xyz.zhno.template.common.lock.annotation.RedisLock;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;

/**
 * 分布式锁切面
 * @author zh_o
 */
@Aspect
@Component
public class RedisLockAspect {

    private final Logger LOG = LoggerFactory.getLogger(RedisLockAspect.class);

    @Resource
    private RedissonClient redissonClient;

    /**
     * 环绕通知
     * @param point 切入点
     */
    @Around("@annotation(xyz.zhno.template.common.lock.annotation.RedisLock)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 记录起始毫秒值
        long startMillis = System.currentTimeMillis();
        // 获取方法对象
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        // 获取方法上注解
        RedisLock annotation = method.getAnnotation(RedisLock.class);
        // 是否有注解
        if (annotation == null) {
            return point.proceed();
        }
        // 获取注解属性
        String key = annotation.value();
        int paramIndex = annotation.index();
        String fieldName = annotation.field();
        long lockTime = annotation.lockTime();
        long timout = annotation.timeout();
        // 定义后缀
        String suffix = null;
        // 判断字段名是否为空
        if (filedIsNotBlank(fieldName)) {
            // 获取传入的参数
            Object param = getParam(paramIndex, point);
            // 获取字段值,作为后缀
            suffix = getFileValue(fieldName, param);
        }
        // 拼接后缀
        if (suffix != null) {
            // 定义分隔符
            final String SEPARATOR = ":";
            StringJoiner sj = new StringJoiner(SEPARATOR);
            key = sj.add(key).add(fieldName).add(suffix).toString();
            // GC
            sj = null;
        }
        LOG.info("当前锁定Key: {}", key);
        // 定义锁
        RLock lock = null;
        // 定义返回值
        Object returnVal;
        try {
            // 获取锁
            lock = redissonClient.getLock(key);
            // 加锁
            lock.tryLock(timout, lockTime, TimeUnit.SECONDS);
            LOG.info("获取到锁，开始执行");
            // 执行业务方法
            returnVal = point.proceed();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 解锁
            if (lock != null) {
                lock.unlock();
            }
            LOG.info("锁已释放");
        }
        LOG.info("一共耗时: {} 毫秒", System.currentTimeMillis() - startMillis);
        return returnVal;
    }

    /**
     * 字段不为空
     * @param filedName 传入字段名
     */
    private boolean filedIsNotBlank(String filedName) {
        final String BLANK_STR = "";
        return !BLANK_STR.equals(filedName.trim());
    }

    /**
     * 参数索引不为空
     * @param paramIndex 传入参数索引
     */
    private boolean paramIndexIsNotEmpty(int paramIndex) {
        return paramIndex >= 0;
    }

    /**
     * 获取参数
     * @param paramIndex 参数索引
     */
    private Object getParam(Integer paramIndex, ProceedingJoinPoint point) {
        // 若指定索引，取指定参数，若未指定索引，取第一个参数
        return paramIndexIsNotEmpty(paramIndex) ? point.getArgs()[paramIndex] : point.getArgs()[0];
    }

    /**
     * 通过反射获取指定字段值
     * @param fieldName 字段名
     * @param obj 方法中参数对象
     * @return 字段值
     */
    private String getFileValue(String fieldName, Object obj) {
        // 拼接 get 方法
        String prefix = "get";
        String getMethodName = prefix + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        // 定义返回值
        String fieldValue;
        try {
            // 获取 get 方法反射对象
            Method method = obj.getClass().getDeclaredMethod(getMethodName);
            // 执行 get 方法获取字段值
            fieldValue = method.invoke(obj).toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return fieldValue;
    }

}
